Skip to content

Implement unique and set operations by sorting - #848

Open
maleadt wants to merge 4 commits into
mainfrom
tb/unique
Open

maleadt wants to merge 4 commits into
mainfrom
tb/unique

Conversation

@maleadt

@maleadt maleadt commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

unique, unique!, allunique, union, intersect, setdiff, symdiff, issubset, issetequal and isdisjoint fell back to Base, which indexes GPU arrays element by element (JuliaGPU/CUDA.jl#991). This implements them for all back-ends using the existing sortperm, sort and logical indexing.

unique sorts the elements so that equal ones are adjacent, marks the first of each group at its original position, and selects the marked elements. This returns them in the same order as Base. unique(A; dims) sorts the slice indices with a comparator that reads the array, which needs Adapt 4.8 (JuliaGPU/Adapt.jl#130). JLArrays' host sort now converts such captured arrays too.

The set operations use unique, and check membership with a binary search in the sorted other array. Elements of different types are compared without converting them. Element types, order and errors match Base on Julia 1.10 to 1.13. Elements are ordered with isless, which complex numbers don't have, so those are ordered by their real and imaginary parts. Membership uses isequal, like Base's sets. Base's issubset and isdisjoint use == for arrays of fewer than 70 elements, which only makes a difference for NaN, ±0.0 and missing.

Not included: arguments mixing GPU and CPU arrays, union! and the other mutating functions, and allequal.

The first commit renames JLArrays' _host and _host_source helpers.

Timings on an RTX 5080, for Int64 elements (median of 10 runs; CPU is a single run of Base):

GPU CPU
unique, 10⁸ elements, 10⁶ distinct 137 ms ~1.9 s
unique(A; dims=2), 8×10⁶ 4.4 ms 381 ms
unique(A; dims=2), 512×10⁴ 0.5 ms 13.6 ms
intersect / setdiff, 10⁷ each 12.5 ms 0.6–1.0 s
union / symdiff, 10⁷ each 22 ms 0.5–1.1 s

Fixes #674

A comparator can capture an array, for example to sort the columns of a matrix
by their contents. Since Adapt 4.8, such arrays are converted when a kernel
captures the ordering, so require that version.
These indexed the array element by element. Instead, sort the elements so
that equal ones are adjacent, mark the first of each group at its original
position, and select the marked elements. This returns them in the order Base
does. `unique(A; dims)` sorts the slice indices with a comparator that
compares slices.

Elements are ordered with `isless`, or with `isless` on the real and imaginary
parts for complex numbers. Element types without `isless` throw an error.

x-ref JuliaGPU/CUDA.jl#991
`union`, `intersect`, `setdiff`, `symdiff`, `issubset`, `issetequal` and
`isdisjoint` indexed the arrays element by element. Build them on `unique`,
and check membership with a binary search in the sorted other array. Elements
of different types are compared without converting them, as `isequal` does.

Element types, order and errors match Base, including the change to
`setdiff`'s element type in Julia 1.13. Membership uses `isequal`, like Base's
sets; Base's `issubset` and `isdisjoint` use `==` for arrays of fewer than 70
elements.

x-ref #674

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scalar indexing for unique and intersect

1 participant